package com.ideaaedi.component.dump;

import com.ideaaedi.commonds.io.IOUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import sun.jvm.hotspot.oops.InstanceKlass;
import sun.jvm.hotspot.tools.jcore.ClassFilter;
import sun.jvm.hotspot.tools.jcore.ClassWriter;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.stream.Collectors;

import static com.ideaaedi.component.dump.ExitDumpClassExecutor.DEFAULT_KEEP_LONG_NAME;

/**
 * 前缀匹配ClassFilter
 *
 * @author JustryDeng
 * @since 2021/9/25 18:48:50
 */
@Slf4j
public class PrefixMatchClassFilter implements ClassFilter {
    
    /** dump文件集合 */
    public static final List<String> DUMP_FILE_PATH_LIST = new CopyOnWriteArrayList<>();
    
    public static final String CLASS_DUMP_FILTER_KEY = "sun.jvm.hotspot.tools.jcore.filter";
    
    public static final String INCLUDE_PREFIXES_KEY = "idea-aedi.dump-class.prefix.includes";
    
    public static final String EXCLUDE_PREFIXES_KEY = "idea-aedi.dump-class.prefix.excludes";
    
    public static final String OUTPUT_DIR_KEY = "idea-aedi.dump-class.output.dir";
    
    public static final String KEEP_LONG_NAME_KEY = "idea-aedi.dump-class.keep-long-name";
    
    /** 要dump的class的全类名前缀匹配集合 */
    private final Set<String> INCLUDE_PREFIX_SET = Collections.synchronizedSet(new HashSet<>());
    
    /** 明确排除dump的class的全类名前缀匹配集合 */
    private final Set<String> EXCLUDE_PREFIX_SET = Collections.synchronizedSet(new HashSet<>());
    
    /** 输出目录 */
    public final String OUTPUT_DIR;
    
    /**
     * 是否以全类名作为文件名
     * <br><br>
     * 假设现有类com.example.HelloWorld，那么：
     * <ul>
     *     <li>为true时：输出的文件为com.example.HelloWorld.class</li>
     *     <li>为false时：：输出的文件为com/example/HelloWorld.class</li>
     * </ul>
     */
    public boolean keepLongName = DEFAULT_KEEP_LONG_NAME;
    
    /**
     * 一定要是无参构造
     */
    public PrefixMatchClassFilter() {
        // INCLUDE_PREFIX_SET
        String includePrefixes = System.getProperty(INCLUDE_PREFIXES_KEY);
        if (StringUtils.isBlank(includePrefixes)) {
            throw new IllegalArgumentException("includePrefixes cannot be blank.");
        }
        this.INCLUDE_PREFIX_SET.addAll(Arrays.stream(includePrefixes.split(","))
                .map(String::trim)
                .filter(StringUtils::isNotBlank)
                .collect(Collectors.toSet()));
    
        // EXCLUDE_PREFIX_SET
        String excludePrefixes = System.getProperty(EXCLUDE_PREFIXES_KEY);
        if (StringUtils.isNotBlank(excludePrefixes)) {
            this.EXCLUDE_PREFIX_SET.addAll(Arrays.stream(excludePrefixes.split(","))
                    .map(String::trim)
                    .filter(StringUtils::isNotBlank)
                    .collect(Collectors.toSet()));
        }
    
        String keepLongNameStr = System.getProperty(KEEP_LONG_NAME_KEY);
        if (StringUtils.isNotBlank(keepLongNameStr)) {
            try {
                keepLongName = Boolean.parseBoolean(keepLongNameStr);
            } catch (Exception e) {
                // ignore
            }
        }
        
        // OUTPUT_DIR
        OUTPUT_DIR = System.getProperty(OUTPUT_DIR_KEY);
        if (StringUtils.isBlank(OUTPUT_DIR)) {
            throw new IllegalArgumentException("OUTPUT_DIR cannot be blank.");
        }
        
        System.out.printf("INCLUDE_PREFIX_SET -> %s, EXCLUDE_PREFIX_SET -> %s, KEEP_LONG_NAME -> %s, OUTPUT_DIR -> %s%n",
                INCLUDE_PREFIX_SET, EXCLUDE_PREFIX_SET, keepLongName, OUTPUT_DIR);
    }
    
    @Override
    public boolean canInclude(InstanceKlass instanceKlass) {
        dumpClassIfNecessary(instanceKlass);
        return false;
    }
    
    /**
     * dump正在运行着的class文件
     *
     * @param instanceKlass
     *            class实例
     */
    private void dumpClassIfNecessary(InstanceKlass instanceKlass) {
        ByteArrayOutputStream outputStream = null;
        try {
            String classFilePath = instanceKlass.getName().asString();
            String classLoneName = classFilePath.replace('/', '.');
            for (String excludePrefix : EXCLUDE_PREFIX_SET) {
                if (classLoneName.startsWith(excludePrefix)) {
                    return;
                }
            }
            for (String includePrefix : INCLUDE_PREFIX_SET) {
                if (!classLoneName.startsWith(includePrefix)) {
                    return;
                }
            }
            File classFile = new File(OUTPUT_DIR, keepLongName ? (classLoneName + ".class") : (classFilePath + ".class"));
            DUMP_FILE_PATH_LIST.add(classFile.getAbsolutePath());
            System.out.println("dumping... " + classLoneName);
            outputStream = new ByteArrayOutputStream();
            ClassWriter cw = new ClassWriter(instanceKlass, outputStream);
            cw.write();
            IOUtil.toFile(outputStream.toByteArray(), classFile, true);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            IOUtil.close(outputStream);
        }
    }
}
